package ua.yandex.collections;

import java.util.Arrays;

public class MyLinkedList implements MyList {
    private int size;
    private Node<Object> first;
    private Node<Object> last;

    private static class Node<E> {
        private Node<E> next;
        private Node<E> previous;
        private E value;

        private Node(Node<E> previous, E value, Node<E> next) {
            this.previous = previous;
            this.value = value;
            this.next = next;

            if (previous != null) {
                previous.next = this;
            }

            if (next != null) {
                next.previous = this;
            }
        }

        private Node(Node<E> previous, E value) {
            this(previous, value, null);
        }

        private Node(E value, Node<E> next) {
            this(null, value, next);
        }

        @Override
        public String toString() {
            Node<E> current = this;

            StringBuilder sb = new StringBuilder();
            String separator = "";

            while (current != null) {
                sb.append(separator)
                        .append(current.value);
                separator = ",";
                current = current.next;
            }

            return sb.toString();
        }
    }

    @Override
    public void add(Object e) {
        if (isEmpty()) {
            first = new Node<>(first, e, last);
            last = first;
        } else {
            last = new Node<>(last, e);
        }

        size++;
    }

    @Override
    public void add(int index, Object e) {
        checkIndexBounds(index);

        if (index == 0) {
            addFirst(e);
        } else {
            Node<Object> foundNode = findNodeByIndex(index);
            new Node<>(foundNode.previous, e, foundNode);
        }

        size++;
    }

    @Override
    public void addAll(Object[] c) {
        for (Object e : c) {
            add(e);
        }
    }

    @Override
    public void addAll(int index, Object[] c) {
        checkIndexBounds(index);

        Node<Object> current = findNodeByIndex(index);
        for (int i = 0; i < c.length; i++) {
            if (current != null) {
                current.value = c[i];
                current = current.next;
            } else {
                addAll(Arrays.copyOfRange(c, i, c.length));
            }
        }
    }

    @Override
    public Object get(int index) {
        checkIndexBounds(index);

        return findNodeByIndex(index).value;
    }

    private Node<Object> findNodeByIndex(int index) {
        Node<Object> current = first;
        for (int i = 0; i < index; i++) {
            current = current.next;
        }
        return current;
    }

    @Override
    public Object remove(int index) {
        checkIndexBounds(index);

        Node<Object> current = findNodeByIndex(index);

        if (current == first && current == last) {
            first = null;
            last = null;
        } else if (current == first) {
            first = first.next;
        } else if (current == last) {
            last = current.previous;
        } else {
            current.previous.next = current.next;
            current.next.previous = current.previous;
        }

        size--;

        return current.value;
    }

    private void checkIndexBounds(int index) {
        if (size <= index || index < 0) {
            String template = "Non-existent index <%d> on list of size %d";
            String message = String.format(template, index, size);
            throw new IndexOutOfBoundsException(message);
        }
    }

    @Override
    public void set(int index, Object e) {
        checkIndexBounds(index);

        findNodeByIndex(index).value = e;
    }

    @Override
    public int indexOf(Object o) {
        Node<Object> current = first;
        for (int i = 0; i < size; i++) {
            Object currentValue = current.value;

            if (o.equals(currentValue)) {
                return i;
            }

            current = current.next;
        }

        return -1;
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public void clear() {
        size = 0;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public Object[] toArray() {
        Object[] array = new Object[size];

        Node<Object> current = first;
        for (int i = 0; i < size; i++) {
            array[i] = current.value;
            current = current.next;
        }

        return array;
    }

    /**
     * Add an element at the beginning of the list.
     *
     * @param e an element to add.
     */
    public void addFirst(Object e) {
        if (isEmpty()) {
            add(e);
        } else {
            first = new Node<>(e, first);
            size++;
        }
    }

    /**
     * Add an element at the end of the list.
     *
     * @param e an element to add.
     */
    public void addLast(Object e) {
        add(e);
    }

    /**
     * Get an element from the beginning of the list.
     *
     * @return an element from the beginning of the list.
     */
    public Object getFirst() {
        return get(0);
    }

    /**
     * Get an element from the end of the list.
     *
     * @return an element from the end of the list.
     */
    public Object getLast() {
        return get(size() - 1);
    }

    /**
     * Remove and return an element from the beginning of the list.
     *
     * @return an element removed from the beginning of the list.
     */
    public Object removeFirst() {
        return remove(0);
    }

    /**
     * Remove and return an element from the end of the list.
     *
     * @return an element removed from the end of the list.
     */
    public Object removeLast() {
        return remove(size() - 1);
    }

    @Override
    public String toString() {
        String representation = "";

        if (!isEmpty()) {
            representation = first.toString();
        }

        return "[" + representation + "]";
    }

}
